package io.github.cubelitblade.librarymanagementbackend.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import io.github.cubelitblade.librarymanagementbackend.dto.UserDTO;
import io.github.cubelitblade.librarymanagementbackend.entity.User;
import io.github.cubelitblade.librarymanagementbackend.exception.BusinessException;
import io.github.cubelitblade.librarymanagementbackend.mapper.UserMapper;
import io.github.cubelitblade.librarymanagementbackend.utils.PasswordHelper;
import io.github.cubelitblade.librarymanagementbackend.vo.UserVO;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.LocalDateTime;

@Service
public class AuthService extends ServiceImpl<UserMapper, User> {
    private final long DEFAULT_SESSION_EXPIRY_TIME = 30L * 24 * 60 * 60;

    private final UserService userService;

    AuthService(UserService userService) {
        this.userService = userService;
    }

    /**
     * 注册新用户。
     * <p>
     * 将根据传入的 {@link UserDTO} 创建新的用户账号，包含密码加盐加密逻辑。
     * 若用户名已存在或数据库操作失败，将抛出 {@link BusinessException} ，并由 {@code GlobalExceptionHandler} 响应。
     *
     * @param userDTO 包含注册信息的 {@link UserDTO}，至少包含用户名和密码。
     * @return 成功注册后的脱敏用户信息 {@link UserVO}。
     * @throws BusinessException 当用户名已存在或数据库插入失败时。
     */
    public UserVO register(UserDTO userDTO) {
        checkBlankFields(userDTO);

        User user = new User();
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUsername, userDTO.getUsername());

        if (getOne(queryWrapper) != null) {
            throw BusinessException.userAlreadyExist();
        } else {
            // 密码使用加盐SHA256处理。处理完成后，在数据库保存加盐密码以及生成的盐。
            String salt = PasswordHelper.generateSalt();

            user.setUsername(userDTO.getUsername());
            user.setPassword(PasswordHelper.saltPassword(userDTO.getPassword(), salt));
            user.setNickname(userDTO.getNickname());
            user.setSalt(salt);
            user.setCreateTime(LocalDate.now());
        }
        if (this.save(user)) {
            return UserVO.of(user, 0L, 0L);
        } else {
            throw BusinessException.databaseError();
        }
    }

    /**
     * 用户登录。
     *
     * <p>根据传入的 {@link UserDTO} 验证用户名与密码是否匹配。
     * 如果登录成功，返回脱敏后的 {@link UserVO} 对象；
     * 若用户名不存在或密码错误，则抛出 {@link BusinessException} ，并由 {@code GlobalExceptionHandler} 响应。</p>
     *
     * @param userDTO 包含用户名和密码的 {@link UserDTO}。
     * @return 登录成功后的用户信息 {@link UserVO}。
     * @throws BusinessException 当用户不存在或密码不正确时。
     */
    public UserVO login(UserDTO userDTO) {
        checkBlankFields(userDTO);

        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUsername, userDTO.getUsername());

        // 查找用户名匹配的候选者，如果不存在则直接抛出异常。
        User candidate = getOne(queryWrapper);

        if (candidate == null) {
            throw BusinessException.userNotFound();
        } else {
            // 当数据库中有用户名匹配的候选者时，将其取出，将用户传入的密码与候选者的盐字符串进行SHA256加盐处理，然后比对结果是否一致。
            // 若一致表示密码正确，返回对应的ID；否则返回-1.
            if (candidate.getPassword().equals(PasswordHelper.saltPassword(userDTO.getPassword(), candidate.getSalt()))) {

                // 创建用户会话时，为其分配会话过期时间。
                candidate.setSessionExpireAt(LocalDateTime.now().plusSeconds(DEFAULT_SESSION_EXPIRY_TIME));
                updateById(candidate);

                // 重新查找当前的用户最新状态，发送给用户。
                return userService.getUser(candidate.getId());
            } else {
                throw BusinessException.passwordIncorrect();
            }
        }
    }

    /**
     * 根据用户ID查询会话过期时间。
     *
     * <p>若会话尚未过期，则更新会话过期时间，保持此会话；若会话不存在或过期，则抛出 {@link BusinessException}，并由 {@code GlobalExceptionHandler} 响应。 </p>
     *
     * @param userId 待查询的用户ID。
     * @return 如果数据库内用户的会话还未过期，则更新会话的过期时间，然后返回用户的 {@link UserVO} 对象。
     * @throws BusinessException 当 {@code userId} 为 {@code null} 、{@code userId} 在数据库内的会话到期时间为 {@code null} 或当前的服务器时间已经超出会话过期时间时。
     */
    public UserVO checkSession(Integer userId) {
        // TODO：严格意义上讲，只通过用户ID检查会话状态并续期是不安全的，暂时没有实现鉴权。如果后续有时间的话实现简单的鉴权。

        // 前端请求的用户ID为空，表示未登录过。
        if (userId == null) {
            throw BusinessException.unauthorized();
        }

        User candidate = getById(userId);

        // 前端请求的用户ID不存在时。
        if (candidate == null) {
            throw BusinessException.userNotFound();
        }

        // 前端请求的用户所对应的会话过期时间为空或当前的服务器时间已经超出会话过期时间时，视为会话过期。
        if (candidate.getSessionExpireAt() == null || candidate.getSessionExpireAt().isBefore(LocalDateTime.now())) {
            throw BusinessException.sessionExpired();
        }

        // 如果没有抛出任何异常，则重设会话过期时间，并且在数据库里面更新这个时间，然后返回用户视图供前端维持登录状态。
        candidate.setSessionExpireAt(LocalDateTime.now().plusSeconds(DEFAULT_SESSION_EXPIRY_TIME));
        updateById(candidate);
        return userService.getUser(candidate.getId());
    }

    /**
     * 校验用户输入是否为空。
     *
     * <p>若用户名或密码为空，将抛出 {@link BusinessException} ，并由 {@code GlobalExceptionHandler} 响应。</p>
     *
     * @param userDTO 要校验的 {@link UserDTO} 对象。
     * @throws BusinessException 当用户名或密码为时。
     */
    private void checkBlankFields(UserDTO userDTO) {
        // 去除用户名的尾部空白字符。
        userDTO.setUsername(userDTO.getUsername().trim());

        if (userDTO.getUsername() == null || userDTO.getUsername().isEmpty() || userDTO.getPassword() == null || userDTO.getPassword().isEmpty()) {
            throw BusinessException.nullKeyField();
        }
    }
}
